"""
In order to maintain a targetting system that can be generic and modular, as well as to allow for handling much of the
nitty-gritty stuff (such as pushing missions correcty) by the Framework rather than by the user, each "target" you
select will be encapsulated into a TargetInfo policy class. The policy class will allow for the targetting systems to
easily be able to get the help info and other target-specific items, as well as allowing the Results function to be able
to easily push missions for targets, all without having to recode the graphics or results subsystem.

For more information on how to use these, visit the [TargetInfo] page.
"""

from CvPythonExtensions import *

gc = CyGlobalContext()

import GerikSpellsInterface

import CvUtil
import copy

class GSBase:
    # All targets have these
    def __init__(self, pTargettableCallback):
        """
        Arguments:
            @pTargettableCallback: The callback that will be used to determine if a target can be targetted by the spell.
        
        Returns:
            >None

        Description:
            >The pTargettableCallback should be a function with args of:
            > (pCastingGroup, pCastingPlot, pPotentialTarget, bVisible)

        <FuncImplemented>
        """
        self.m_pTarget = None
        self.m_iData1 = -1
        self.m_iData2 = -1
        self.m_iData3 = -1
        self.m_pTargettableCallback = pTargettableCallback

    def getData1(self):
        """
        Arguments:
            @None
        
        Returns:
            >Returns the value that is used as iData1.

        Description:
            >None

        <FuncImplemented>
        """
        return self.m_iData1

    def setData1(self, iNewData1):
        """
        Arguments:
            @iNewData1: The new variable to be used as iData1.
        
        Returns:
            >None

        Description:
            >Change the value of an iData1 variable. Note that if iData1 is used to determine the target (such as if it
            represents a unit's owner) that the self.m_pTarget should be updated as well during this function!
        <FuncMaybeImplemented>
        """
        self.m_iData1 = iNewData1

    def getData2(self):
        """
        Arguments:
            @None
        
        Returns:
            >Returns the value that is used as iData2.

        Description:
            >None.

        <FuncImplemented>
        """
        return self.m_iData2

    def setData2(self, iNewData2):
        """
        Arguments:
            @iNewData2: The new variable to be used as iData1.
        
        Returns:
            >None

        Description:
            >Change the value of an iData2 variable. Note that if iData2 is used to determine the target (such as if it
            represents a unit's owner) that the self.m_pTarget should be updated as well during this function!
        <FuncMaybeImplemented>
        """
        self.m_iData2 = iNewData2

    def getData3(self):
        """
        Arguments:
            @None
        
        Returns:
            >Returns the value that is used as iData3.

        Description:
            >None.

        <FuncImplemented>
        """
        return self.m_iData3

    def setData3(self, iNewData3):
        """
        Arguments:
            @iNewData3: The new variable to be used as iData3.
        
        Returns:
            >None

        Description:
            >Change the value of an iData3 variable. Note that if iData3 is used to determine the target (such as if it
            represents a unit's owner) that the self.m_pTarget should be updated as well during this function!
        <FuncMaybeImplemented>
        """
        self.m_iData3 = iNewData3

    def getTarget(self):
        """
        Arguments:
            @None
        
        Returns:
            >The data that is the actual target (such as a plot, unit, integer type for an improvement, etc).

        Description:
            >None

        <FuncImplemented>
        """
        return self.m_pTarget

    def setTarget(self, pTarget):
        """
        Arguments:
            @pTarget: The new target object (such as a CyPlot, CyUnit, integer type for an improvement, etc.).
        
        Returns:
            >None

        Description:
            >Should override this to also set the new iData1, iData2, and iData3 variables.

        <FuncMaybeImplemented>
        """
        self.m_pTarget = pTarget

    def setTargetWithData(self, iData1, iData2, iData3):
        """
        Arguments:
            @iData1: iData1 variable.
            @iData2: iData2 variable.
            @iData3: iData3 variable.

        Returns:
            >None

        Description:
            > Sets the target data based upon iData1, iData2, and iData3. You should override this if making
            your own TargetInfo policy so that when setting the iData's you can properly construct your target.

        <FuncMaybeImplemented>
        """
        self.m_iData1 = iData1
        self.m_iData2 = iData2
        self.m_iData3 = iData3

    # isPlotTargetPolicy is overridden by plot targets.
    def isPlotTargetPolicy(self):
        """
        Arguments:
            @None
        
        Returns:
            >True if the target should act like a plot target.

        Description:
            >None

        <FuncImplemented>
        """
        return False

    def isCurrentTargetValid(self, pCastingGroup, pCastingPlot):
        """
        Arguments:
            @pCastingGroup: The group casting the spell.
            @pCastingPlot: The plot that the group casting the spell casts it from.
        
        Returns:
            >True if the current target (or the list of targets it produces) can be targetted.
            >False otherwise.

        Description:
            >In most cases, this will just check the target, but in some cases (like a target of multiple plots)
            the "target" is really a list of target. Returns true if ANY targets in that list can be affected.

        <FuncImplemented>
        """
        return len(self.getAffectedTargetList(pCastingGroup, pCastingPlot)) > 0
        

    def isTargettable(self, pCastingGroup, pCastingPlot, pPotentialTarget, bVisible):
        """
        Arguments:
            @pCastingGroup: The group casting the spell.
            @pCastingPlot: The plot that the group casting the spell casts it from.
            @pPotentialTarget: The potential target that we're checking.
            @bVIsible: True if just checking if we can visably show the target in the targetting screen, false otherwise.

        Returns:
            >True if the target we're checking is targettable. False otherwise.

        Description:
            >An example of the bVisible flag's use is selecting a unit with the pie menu. Passing a target with bVisible
            True by not passing if it's false means that the target will show up in the pie menu, but it won't be clickable.

        <FuncImplemented>
        """
        if (self.m_pTargettableCallback):
            return self.m_pTargettableCallback(pCastingGroup, pCastingPlot, pPotentialTarget, bVisible)

        return True

    def getAffectedTargetList(self, pCastingGroup, pCastingPlot):
        """
        Arguments:
            @pCastingGroup: The group casting the spell.
            @pCastingPlot: The plot that the group casting the spell casts it from.
        
        Returns:
            >A list of targets that will be affected, made from looking at the one target.

        Description:
            >An example is that if a multi-plot target info is used, then getTarget() will return the plot,
            but getAffectedTargetList will return all the plots that will be affected.

            
            >Typically, getSecondaryTargetList will want to get both the affected
            and unaffected target list. So, it's preferable that we do it all at
            once to avoid iterating through all the possible targets twice.
            If it is only necessary to have one list or the other, these
            are helper functions that can do it for us. However, it's a bad
            idea to call getAffectedTargetList right after getUnaffectedTargetList.

        <FuncImplemented>
        """
        return self.getSecondaryTargetList(pCastingGroup, pCastingPlot)[0]

    def getUnaffectedTargetList(self, pCastingGroup, pCastingPlot):
        """
        Arguments:
            @pCastingGroup: The group casting the spell.
            @pCastingPlot: The plot that the group casting the spell casts it from.
        
        Returns:
            > Same as getAffectedTargetList, but returns the targets that will NOT be affected.
            
        Description:
            >None

        <FuncImplemented>
        """
        return self.getSecondaryTargetList(pCastingGroup, pCastingPlot)[1]

    def getSecondaryTargetList(self, pCastingGroup, pCastingPlot):
        """
        Arguments:
            @pCastingGroup: The group casting the spell.
            @pCastingPlot: The plot that the group casting the spell casts it from.
        
        Returns:
            > List of all units that will be shown in the targetting system. 
            

        Description:
            >Some targets returned may be affected, some may not, but they will all show in the targetting system one way or another.

        <FuncImplemented>
        """
        raise NotImplementedError

class GSNoTarget(GSBase):
    def __init__(self):
        GSBase.__init__(self, None)

    def parseInfo(self, bStrategyOption):
        return ""

    def getBars(self):
        return []

    def getSecondaryTargetList(self):
        return []
        
PLOT_RANGE_AFFECT_RULE_SELECTED = 0
PLOT_RANGE_AFFECT_RULE_GLOBAL = 1
PLOT_RANGE_AFFECT_RULE_INDIVIDUAL = 2

class GSPlotTarget(GSBase):
    def __init__(self, pTargettableCallback=None):
        GSBase.__init__(self, pTargettableCallback)

        self.m_bCheckVisible = True
        self.m_bCheckRange = False

        self.m_iRangeAffectRule = PLOT_RANGE_AFFECT_RULE_SELECTED
        self.m_pRangeMakerCallback = None
        self.m_pRangePlotList = None # Will be set during the stage's activation.

    def setData1(self, iData1):
        GSBase.setData1(self, iData1)
        GSBase.setTarget(self, CyMap().plot(iData1, self.getData2()) )

    def setData2(self, iData2):
        GSBase.setData2(self, iData2)
        GSBase.setTarget(self, CyMap().plot(self.getData1(), iData2) )

    def setTargetWithData(self, iData1, iData2, iData3=-1):
        GSBase.setTargetWithData(self, iData1, iData2, iData3)
        GSBase.setTarget(self, CyMap().plot(iData1, iData2) )

    def setTarget(self, pTarget):
        GSBase.setTarget(self, pTarget)
        GSBase.setData1(self, pTarget.getX())
        GSBase.setData2(self, pTarget.getY())

    def isPlotTargetPolicy(self):
        return True

    def isTargettable(self, pCastingGroup, pCastingPlot,  pPotentialTarget, bVisible):
        if (self.m_bCheckVisible):
            if not pPotentialTarget.isVisible(pCastingGroup.getTeam(), False):
                return False

        if (self.m_bCheckRange):
            iPotTargetX = pPotentialTarget.getX()
            iPotTargetY = pPotentialTarget.getY()
            bFound = False
            for pPlot in self.getRangePlotList(pCastingGroup, pCastingPlot):
                if (iPotTargetX == pPlot.getX() and iPotTargetY == pPlot.getY()):
                    bFound = True
                    break

            if (not bFound):
                return False

        if (self.m_pTargettableCallback):
            return self.m_pTargettableCallback(pCastingGroup, pCastingPlot, pPotentialTarget, bVisible)

        return True

    def getCheckRange(self):
        return self.m_bCheckRange

    def setCheckRange(self, bCheck):
        self.m_bCheckRange = bCheck

    def getCheckVisibility(self):
        return self.m_bCheckVisibile

    def setCheckVisibility(self, bCheck):
        self.m_bCheckVisible = bCheck

    def getRangeMakerCallback(self):
        return self.m_pRangeMakerCallback

    def setRangeMakerCallback(self, pRangeMakerCallback):
        self.m_pRangeMakerCallback = pRangeMakerCallback

    def getRangePlotList(self, pCastingGroup, pCastingPlot):
        if (self.m_pRangePlotList):
            return self.m_pRangePlotList
        else:
            pRangePlotList = []
            if (self.getRangeMakerCallback()):
                pRangePlotList = self.getRangeMakerCallback()(self.m_pTarget)
            self.setRangePlotList(pRangePlotList)
            return self.m_pRangePlotList

    def setRangePlotList(self, pRangePlotList):
        self.m_pRangePlotList = pRangePlotList

    def getRangeAffectRule(self):
        return self.m_iRangeAffectRule

    def setRangeAffectRule(self, iNewRule):
        self.m_iRangeAffectRule = iNewRule
        
class GSSinglePlotTarget(GSPlotTarget):
    def isAffectedTarget(self, pCastingGroup, pCastingPlot, pTarget):
        if self.isTargettable(pCastingGroup, pCastingPlot, pTarget, False):
            return True

    def getSecondaryTargetList(self, pCastingGroup, pCastingPlot):
        pAffectedList = []
        pUnaffectedList = []
        if ( self.isAffectedTarget(pCastingGroup, pCastingPlot, self.m_pTarget) ):
            pAffectedList.append(self.m_pTarget)
        else:
            pUnaffectedList.append(self.m_pTarget)

        return (pAffectedList, pUnaffectedList)

class GSSquarePlotTarget(GSPlotTarget):
    def __init__(self, pTargetTestCallback, iSquareSize):
        GSPlotTarget.__init__(self, pTargetTestCallback)
        self.m_iSquareSize = iSquareSize

    def setSquareSize(self, iSquareSize):
        self.m_iSquareSize = iSquareSize

    def isAffectedTarget(self, pCastingGroup, pCastingPlot, pTarget):
        if (not self.isTargettable(pCastingGroup, pCastingPlot, pTarget, False)):
            return False

        iSquareCenterX = self.m_pTarget.getX()
        iSquareCenterY = self.m_pTarget.getY()
        iSquareSize = self.m_iSquareSize
        if (abs(pTarget.getX() - iSquareCenterX) <= iSquareSize and
            abs(pTarget.getY() - iSquareCenterY) <= iSquareSize):
            return True

    def getSecondaryTargetList(self, pCastingGroup, pCastingPlot):
        pAffectedList = []
        pUnaffectedList = []
        iSquareSize = self.m_iSquareSize
        pTargetPlot = self.m_pTarget
        # Just test the targetted plot, and set all to the result
        if (self.getRangeAffectRule() == PLOT_RANGE_AFFECT_RULE_SELECTED):
            if (self.isAffectedTarget(pCastingGroup, pCastingPlot, self.m_pTarget)):
                for iX in range(-1 * iSquareSize, iSquareSize + 1):
                    for iY in range(-1 * iSquareSize, iSquareSize + 1):
                        pAffectedList.append( CyMap().plot(iX + pTargetPlot.getX(), iY + pTargetPlot.getY() ) )
            else:
                for iX in range(-1 * iSquareSize, iSquareSize + 1):
                    for iY in range(-1 * iSquareSize, iSquareSize + 1):
                        pUnaffectedList.append( CyMap().plot(iX + pTargetPlot.getX(), iY + pTargetPlot.getY() ) )
                
        # Test each plot, and if any one plot fails, then they will all fail.
        elif (self.getRangeAffectRule() == PLOT_RANGE_AFFECT_RULE_GLOBAL):
            pPlotList = []
            bPass = True
            for iX in range(-1 * iSquareSize, iSquareSize + 1):
                for iY  in range(-1 * iSquareSize, iSquareSize + 1):
                    pPlot = CyMap().plot(iX + pTargetPlot.getX(), iY + pTargetPlot.getY() )
                    pPlotList.append(pPlot)
                    if (not self.isAffectedTarget(pCastingGroup, pCastingPlot, pPlot) ):
                        bPass = False

            if bPass:
                pAffectedList = pPlotList
            else:
                pUnaffectedList = pPlotList
            

        # Test each plot individually
        elif (self.getRangeAffectRule() == PLOT_RANGE_AFFECT_RULE_INDIVIDUAL):
            for iX in range(-1 * iSquareSize, iSquareSize + 1):
                for iY in range(-1 * iSquareSize, iSquareSize + 1):
                    pTestPlot = CyMap().plot(iX + pTargetPlot.getX(), iY + pTargetPlot.getY() )
                    if (self.isAffectedTarget(pCastingGroup, pCastingPlot, pTestPlot) ):
                        pAffectedList.append(pTestPlot)
                    else:
                        pUnaffectedList.append(pTestPlot)

        else:
            CvUtil.pyAssert(false, "Error making range targets... No known range affect value")

        return (pAffectedList, pUnaffectedList)

class GSUnitTarget(GSBase):
    def __init__(self, pTargettableCallback):
        GSBase.__init__(self, pTargettableCallback)

    def setData1(self, iData1):
        GSBase.setData1(self, iData1)
        GSBase.setTarget(self, iData1)

    def setTargetWithData(self, iData1, iData2=-1, iData3=-1):
        GSBase.setTargetWithData(self, iData1, iData2, iData3)
        GSBase.setTarget(self, iData1)

    def setTarget(self, pTarget):
        GSBase.setTarget(self, pTarget)
        GSBase.setData1(self, pTarget)

    def parseInfo(self, bStrategyOption):
        return CyGameTextMgr().getUnitHelp(self.m_pTarget, False, bStrategyOption, False, None)

    def getBars(self):
        return []

    def getButton(self):
        return gc.getUnitInfo(self.m_pTarget).getButton()

    def getTargettableList(self, pCastingGroup, pCastingPlot, pPlotList): # pPlotList ignored
        pList = []
        for iI in range(0, gc.getNumUnitInfos()):
            if (self.isTargettable(pCastingGroup, pCastingPlot, iI, True)):
                pList.append(iI)

        return pList

    def isAffectedTarget(self, pCastingGroup, pCastingPlot, pTarget):
        return pTarget == self.m_pTarget

    def getSecondaryTargetList(self, pCastingGroup, pCastingPlot):
        pAffectedList = []
        pUnaffectedList = []
        if (self.isAffectedTarget(pCastingGroup, pCastingPlot, self.m_pTarget)):
            pAffectedList.append(self.m_pTarget)

        return (pAffectedList, pUnaffectedList)

class GSSpecificUnitTarget(GSBase):
    def __init__(self, pTargettableCallback):
        GSBase.__init__(self, pTargettableCallback)

    def setData1(self, iData1):
        GSBase.setData1(self, iData1)
        GSBase.setTarget(self, gc.getPlayer(self.getData1()).getUnit(self.getData2()))

    def setData2(self, iData2):
        GSBase.setData2(self, iData2)
        GSBase.setTarget(self, gc.getPlayer(self.getData1()).getUnit(self.getData2()))

    def setTargetWithData(self, iData1, iData2, iData3=-1):
        GSBase.setTargetWithData(self, iData1, iData2, iData3)
        GSBase.setTarget(self, gc.getPlayer(self.getData1()).getUnit(self.getData2()))

    def setTarget(self, pTarget):
        GSBase.setTarget(self, pTarget)
        GSBase.setData1(self, pTarget.getOwner())
        GSBase.setData2(self, pTarget.getID())

    def parseInfo(self, bStrategyOption):
        return CyGameTextMgr().getSpecificUnitHelp(self.m_pTarget, False, False)

    def getBars(self):
        bShowHealth = False
        if (self.m_pTarget.isFighting()):
                bShowHealth = False
        elif (self.m_pTarget.getDomainType() == DomainTypes.DOMAIN_AIR):
                bShowHealth = self.m_pTarget.canAirAttack()
        else:
                bShowHealth = self.m_pTarget.canFight()
        
        if bShowHealth:
            if (self.m_pTarget.getDamage() >= ((self.getTarget().maxHitPoints() * 2) / 3)):
                return [(self.m_pTarget.currHitPoints(), self.getTarget().maxHitPoints(), "COLOR_RED")]
            elif (self.m_pTarget.getDamage() >= (self.getTarget().maxHitPoints() / 3)):
                return [(self.m_pTarget.currHitPoints(), self.getTarget().maxHitPoints(), "COLOR_YELLOW")]
            else:
                return [(self.m_pTarget.currHitPoints(), self.getTarget().maxHitPoints(), "COLOR_GREEN")]

    def getButton(self):
        return gc.getUnitInfo(self.m_pTarget.getUnitType()).getButton()

    def getTargettableList(self, pCastingGroup, pCastingPlot, pPlotList): 
        pList = []
        for pPlot in pPlotList:
            for iI in range(0, pPlot.getNumUnits()):
                pUnit = pPlot.getUnit(iI)
                if (self.isTargettable(pCastingGroup, pCastingPlot, pUnit, True)):
                    pList.append(pUnit)

        return pList

    def isAffectedTarget(self, pCastingGroup, pCastingPlot, pTarget):
        return pTarget == self.m_pTarget

    def getSecondaryTargetList(self, pCastingGroup, pCastingPlot):
        pAffectedList = []
        pUnaffectedList = []
        if (self.isAffectedTarget(pCastingGroup, pCastingPlot, self.m_pTarget) ):
            pAffectedList.append(self.m_pTarget)
        return (pAffectedList, pUnaffectedList)

class GSSpecificBuildTarget(GSBase):
    def __init__(self, pTargettableCallback):
        GSBase.__init__(self, pTargettableCallback)

    """
    GERIKESTODO: Make this have plot as data1, data2
    """
    def setData1(self, iData1):
        GSBase.setData1(self, iData1)
        GSBase.setTarget(self, iData1)

    def setTargetWithData(self, iData1, iData2=-1, iData3=-1):
        GSBase.setTargetWithData(self, iData1, iData2, iData3)
        GSBase.setTarget(self, iData1)

    def setTarget(self, pTarget):
        GSBase.setTarget(self, pTarget)
        GSBase.setData1(self, pTarget)

    def parseInfo(self, bStrategyOption):
        return gc.getBuildInfo(self.m_pTarget).getDescription()

    def getBars(self):
        return []

    def getButton(self):
        return gc.getBuildInfo(self.m_pTarget).getButton()

    def getTargettableList(self, pCastingGroup, pCastingPlot, pPlotList):
        pList = []
        for iI in range(0, gc.getNumBuildInfos()):
            for pPlot in pPlotList:
                if self.isTargettable(pCastingGroup, pPlot, iI, True):
                     pList.append(iI)
                     break

        return pList

    def isAffectedTarget(self, pCastingGroup, pCastingPlot, pTarget):
        if (pTarget == self.m_pTarget):
            return True

    def getSecondaryTargetList(self, pCastingGroup, pCastingPlot):
        pAffectedList = []
        pUnaffectedList = []
        if (self.isAffectedTarget(pCastingGroup, pCastingPlot, self.m_pTarget) ):
            pAffectedList.append(self.m_pTarget)
        return (pAffectedList, pUnaffectedList)

class GSBuildingTarget(GSBase):
    def __init__(self, pTargettableCallback):
        GSBase.__init__(self, pTargettableCallback)

    def setData1(self, iData1):
        GSBase.setData1(self, pTarget)
        GSBase.setTarget(self, pTarget)

    def setTargetWithData(self, iData1, iData2=-1, iData3=-1):
        GSBase.setTargetWithData(self, iData1, iData2, iData3)
        GSBase.setTarget(self, pTarget)

    def setTarget(self, pTarget):
        GSBase.setTarget(self, pTarget)
        GSBase.setData1(self, pTarget)

    def parseInfo(self, bStrategyOption):
        return gc.getBuildingInfo(self.m_pTarget).getDescription()

    def getBars(self):
        return []

    def getButton(self):
        return gc.getBuildingInfo(self.m_pTarget).getButton()

    def getTargettableList(self, pCastingGroup, pCastingPlot, pPlotList): # pPlotList ignored
        pList = []
        for iI in range(0, gc.getNumBuildingInfos()):
            if self.isTargettable(pCastingGroup, pCastingPlot, iI, True):
                pList.append(iI)

        return pList

    def isAffectedTarget(self, pCastingGroup, pCastingPlot, pTarget):
        if (pTarget == self.m_pTarget):
            return True

    def getSecondaryTargetList(self, pCastingGroup, pCastingPlot):
        pAffectedList = []
        pUnaffectedList = []
        if (self.isAffectedTarget(pCastingGroup, pCastingPlot, self.m_pTarget) ):
            pAffectedList.append(self.m_pTarget)
        return (pAffectedList, pUnaffectedList)
